/*
 * The JAnalytics Library provides a Java API to the Google Analytics service.
 * Copyright (C) 2007 Ansir Development Limited (http://www.ansir.ca)
 * 
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

package ca.ansir.analytics.reports;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import ca.ansir.analytics.util.Utilities;

/**
 * The <code>JAnalytics</code> class is used to login the the Google Analytics
 * service with an email and a password. After a successful login then any of
 * the reporting methods may be invoked. Typical use will include a login
 * followed by obtaining one or more reports. After all required reports are
 * obtained then the <code>JAnalytics</code> should be allowed to go out of
 * scope and a new <code>JAnalytics</code> may be created if more reports are
 * required in the future.
 * 
 * @author Dan Andrews
 */
public class JAnalytics {

	/**
	 * The password constant is used as a parameter name to post to the Google
	 * login service.
	 */
	public static final String PASSWORD_ATTRIBUTE_NAME = "Passwd";

	/**
	 * The email constant is used as a parameter name to post to the Google
	 * login service.
	 */
	public static final String EMAIL_ATTRIBUTE_NAME = "Email";

	/** The filter mode attribute name. */
	public static final String FILTER_MODE_ATTRIBUTE_NAME = "qtyp";

	/** The filter attribute name. */
	public static final String FILTER_ATTRIBUTE_NAME = "q";

	/** The end date attribute name. */
	public static final String END_DATE_ATTRIBUTE_NAME = "ed";

	/** The start date attribute name. */
	public static final String START_DATE_ATTRIBUTE_NAME = "bd";

	/** The report id attribute name. */
	public static final String REPORT_ID_ATTRIBUTE_NAME = "rid";

	/** The report type attribute name. */
	public static final String REPORT_TYPE_ATTRIBUTE_NAME = "vid";

	/** The date format attribute name. */
	public static final String DATE_FORMAT_ATTRIBUTE_NAME = "dateFormat";

	/** The row index attribute name. */
	public static final String ROW_INDEX_ATTRIBUTE_NAME = "tst";

	/** The row index attribute name. */
	public static final String ROW_COUNT_ATTRIBUTE_NAME = "trows";

	/**
	 * The default row count. This is the maximum page size the Google Analytics
	 * will return.
	 */
	public static final int DEFAULT_ROW_COUNT = 500;

	/** The date format attribute name. */
	public static final String DEFAULT_USER_DATE_FORMAT = "dd/MM/yyyy";

	/** Select end tag used to determine the end of the profile list. */
	private static final String SELECT = "</select>";

	/** Used to determine the start of the profile list. */
	private static final String NAME_PROFILE_LIST = "name=\"profile_list\"";

	/**
	 * Used to determine the start of the profile list seems to be now changed
	 * to "profile"
	 */
	private static final String NAME_PROFILE_LIST2 = "id=\"profile\"";

	/** Used to determine the start of a report id value. */
	private static final String OPTION_VALUE = "<option value=";

	/** The date format pattern for the report get request. */
	public static final String GOOGLE_DATE_PATTERN = "yyyyMMdd";

	/** The <code>DateFormat</code> for the report get request. */
	public static final DateFormat GOOGLE_DATE_FORMAT = new SimpleDateFormat(
			GOOGLE_DATE_PATTERN);

	/** The <code>HttpClient</code> for the Google Analytics service. */
	private HttpClient client;

	/** A list of report ids. */
	private List reportIdList = new ArrayList();

	private String latestFetchedSource;

	/**
	 * Constructs a new <code>JAnalytics</code> object.
	 */
	public JAnalytics() {
	}

	/**
	 * The login method must be invoked prior to invoking any of the reporting
	 * methods.
	 * 
	 * @param email
	 *            The a valid Google Analytics email account.
	 * @param password
	 *            The corresponding Google Analytics email password.
	 * @throws JAnalyticsException
	 */
	public void login(String email, String password) throws JAnalyticsException {
		login(email, password, Languages.ENGLISH_US);
	}

	/**
	 * This method is currently private and only ENGLISH_US logins are allowed
	 * for now.
	 * 
	 * @param email
	 *            The a valid Google Analytics email account.
	 * @param password
	 *            The corresponding Google Analytics email password.
	 * @throws JAnalyticsException
	 */
	private void login(String email, String password, Languages language)
			throws JAnalyticsException {
		if (email == null) {
			throw new IllegalArgumentException("Email cannot be null.");
		}
		if (password == null) {
			throw new IllegalArgumentException("Password cannot be null.");
		}
		if (language == null) {
			throw new IllegalArgumentException("Language cannot be null.");
		}

		if (client == null) {
			client = new HttpClient();
		}
		client.getHostConfiguration().setHost("www.google.com", 443, "https");
		// client.getHostConfiguration().setProxy("localhost", 8888);

		client.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);

		// Prepare login parameters
		NameValuePair action = new NameValuePair("continue",
				"https://www.google.com/analytics/home");
		NameValuePair service = new NameValuePair("service", "analytics");
		NameValuePair nui = new NameValuePair("nui", "hidden");
		NameValuePair hl = new NameValuePair("hl", "en-US");
		NameValuePair gat3t = new NameValuePair("GA3T", "ouVrvynQwUs");
		NameValuePair accountId = new NameValuePair(EMAIL_ATTRIBUTE_NAME, email);
		NameValuePair persistent = new NameValuePair("PersistentCookie", "yes");
		NameValuePair pass = new NameValuePair(PASSWORD_ATTRIBUTE_NAME,
				password);

		PostMethod postMethod = new PostMethod(
				"https://www.google.com/accounts/ServiceLoginBoxAuth");

		NameValuePair[] nameValuePairs = new NameValuePair[] { action, service,
				nui, hl, gat3t, accountId, persistent, pass, };

		postMethod.setRequestBody(nameValuePairs);

		try {
			client.executeMethod(postMethod);

			int statuscode = postMethod.getStatusCode();

			if (statuscode == HttpStatus.SC_OK) {
				BufferedReader reader = new BufferedReader(
						new InputStreamReader(postMethod
								.getResponseBodyAsStream()));

				StringBuffer buf = new StringBuffer();
				for (String line = reader.readLine(); line != null; line = reader
						.readLine()) {
				}
				postMethod.releaseConnection();

				GetMethod redirect = new GetMethod(
						"https://www.google.com/analytics/home");
				client.executeMethod(redirect);
				if (statuscode == HttpStatus.SC_OK) {
					reader = new BufferedReader(new InputStreamReader(redirect
							.getResponseBodyAsStream()));
					buf = new StringBuffer();
					for (String line = reader.readLine(); line != null; line = reader
							.readLine()) {
						buf.append(line);
					}
					redirect.releaseConnection();
					latestFetchedSource = buf.toString();
					initReportIds(buf);
				}
			}
		} catch (IOException e) {
			throw new JAnalyticsException("Login Failure: " + e.getMessage(), e);
		}

	}

	/**
	 * Initializes the report id list if the login is successful .
	 * 
	 * @param analyticsHome
	 *            The buffered analytics home page.
	 * @throws JAnalyticsException
	 */
	private void initReportIds(StringBuffer analyticsHome)
			throws JAnalyticsException {
		int index = analyticsHome.indexOf(NAME_PROFILE_LIST);
		if (index < 0) {
			index = analyticsHome.indexOf(NAME_PROFILE_LIST2);
			if (index < 0) {
				throw new JAnalyticsException(
						"Login Failure: Please ensure account "
								+ "name and password are correct.");
			}
		}
		analyticsHome.delete(0, index);
		index = analyticsHome.indexOf(SELECT);
		if (index < 0) {
			throw new JAnalyticsException(
					"Login Failure: profile_list options not found.");
		}
		analyticsHome.delete(index, analyticsHome.length());
		reportIdList.clear();
		for (index = analyticsHome.indexOf(OPTION_VALUE); index >= 0; index = analyticsHome
				.indexOf(OPTION_VALUE)) {
			analyticsHome.delete(0, index + OPTION_VALUE.length() + 1);
			int nextIndex = analyticsHome.indexOf("\"");
			String reportId = analyticsHome.substring(0, nextIndex);
			if (!"0".equals(reportId)) {
				reportIdList.add(reportId);
			}
		}
		if (reportIdList.size() == 0) {
			throw new JAnalyticsException(
					"Login Failure: no reportIds were found.");
		}
	}

	/**
	 * Gets the array of report ids.
	 * 
	 * @return The array of report ids.
	 */
	public String[] getReportIds() {
		// protects the internal representation.
		return (String[]) reportIdList.toArray(new String[reportIdList.size()]);
	}

	/**
	 * Gets the given report as an indented xml string.
	 * 
	 * @param reportId
	 *            The report id that must be one of the values returned by the
	 *            <code>getReportIds</code> method.
	 * @param reportType
	 *            The report type which cannot be null.
	 * @param startTime
	 *            The start time which cannot be null.
	 * @param endTime
	 *            The end time which cannot be null and must be equal to or
	 *            later than the start time.
	 * @return The report as an indented xml string.
	 * @throws JAnalyticsException
	 */
	public String getReport(String reportId, ReportTypes reportType,
			Calendar startTime, Calendar endTime) throws JAnalyticsException {
		return getReport(reportId, reportType, startTime, endTime, null,
				FilterModes.NONE, 0, DEFAULT_ROW_COUNT);
	}

	/**
	 * Gets the given report as an indented xml string.
	 * 
	 * @param reportId
	 *            The report id that must be one of the values returned by the
	 *            <code>getReportIds</code> method.
	 * @param reportType
	 *            The report type which cannot be null.
	 * @param startTime
	 *            The start time which cannot be null.
	 * @param endTime
	 *            The end time which cannot be null and must be equal to or
	 *            later than the start time.
	 * @param filter
	 *            A named filter or null if none.
	 * @param filterMode
	 *            The filter mode.
	 * @param rowIndex
	 *            The zero-based row index in the report dataset to start the
	 *            export at.
	 * @param rowCount
	 *            The number of dataset rows to return. Google Analytics won't
	 *            return more than 500 rows, setting this higher than 500 will
	 *            still give you 500 rows.
	 * @return The report as an indented xml string.
	 * @throws JAnalyticsException
	 */
	public String getReport(String reportId, ReportTypes reportType,
			Calendar startTime, Calendar endTime, String filter,
			FilterModes filterMode, int rowIndex, int rowCount)
			throws JAnalyticsException {
		return getReportString(reportId, reportType, startTime, endTime,
				filter, filterMode, rowIndex, rowCount);
	}

	/**
	 * Gets the given report as an xml <code>Document</code> object.
	 * 
	 * @param reportId
	 *            The report id that must be one of the values returned by the
	 *            <code>getReportIds</code> method.
	 * @param reportType
	 *            The report type which cannot be null.
	 * @param startTime
	 *            The start time which cannot be null.
	 * @param endTime
	 *            The end time which cannot be null and must be equal to or
	 *            later than the start time.
	 * @return The xml <code>Document</code> object.
	 * @deprecated Not much point in returning a Document if we aren't
	 *             supporting any useful transformations.
	 * @throws JAnalyticsException
	 */
	public Document getReportDocument(String reportId, ReportTypes reportType,
			Calendar startTime, Calendar endTime) throws JAnalyticsException {
		return getReportDocument(reportId, reportType, startTime, endTime,
				null, FilterModes.NONE, 0, DEFAULT_ROW_COUNT);
	}

	/**
	 * Gets the given report as an xml <code>Document</code> object.
	 * 
	 * @param reportId
	 *            The report id that must be one of the values returned by the
	 *            <code>getReportIds</code> method.
	 * @param reportType
	 *            The report type which cannot be null.
	 * @param startTime
	 *            The start time which cannot be null.
	 * @param endTime
	 *            The end time which cannot be null and must be equal to or
	 *            later than the start time.
	 * @param filter
	 *            A named filter or null if none.
	 * @param filterMode
	 *            The filter mode.
	 * @param rowIndex
	 *            The zero-based row index in the report dataset to start the
	 *            export at.
	 * @param rowCount
	 *            The number of dataset rows to return. Google Analytics won't
	 *            return more than 500 rows, setting this higher than 500 will
	 *            still give you 500 rows.
	 * @return The xml <code>Document</code> object.
	 * @deprecated Not much point in returning a Document if we aren't
	 *             supporting any useful transformations.
	 * @throws JAnalyticsException
	 */
	public Document getReportDocument(String reportId, ReportTypes reportType,
			Calendar startTime, Calendar endTime, String filter,
			FilterModes filterMode, int rowIndex, int rowCount)
			throws JAnalyticsException {
		Document document = null;
		try {
			DocumentBuilder builder = DocumentBuilderFactory.newInstance()
					.newDocumentBuilder();
			document = builder
					.parse(getReportInputStream(reportId, reportType,
							startTime, endTime, filter, filterMode, rowIndex,
							rowCount));

		} catch (IOException e) {
			throw new JAnalyticsException("Exception while getting report: "
					+ e.getMessage(), e);
		} catch (ParserConfigurationException e) {
			throw new JAnalyticsException("Exception while getting report: "
					+ e.getMessage(), e);
		} catch (SAXException e) {
			throw new JAnalyticsException("Exception while getting report: "
					+ e.getMessage(), e);
		}
		return document;
	}

	/**
	 * Gets the given report as an <code>InputStream</code> object.
	 * 
	 * @param reportId
	 *            The report id that must be one of the values returned by the
	 *            <code>getReportIds</code> method.
	 * @param reportType
	 *            The report type which cannot be null.
	 * @param startTime
	 *            The start time which cannot be null.
	 * @param endTime
	 *            The end time which cannot be null and must be equal to or
	 *            later than the start time.
	 * @return The <code>InputStream</code> object.
	 * @throws JAnalyticsException
	 */
	public InputStream getReportInputStream(String reportId,
			ReportTypes reportType, Calendar startTime, Calendar endTime)
			throws JAnalyticsException {
		return getReportInputStream(reportId, reportType, startTime, endTime,
				null, FilterModes.NONE, 0, DEFAULT_ROW_COUNT);
	}

	/**
	 * Gets the given report as an <code>InputStream</code> object.
	 * 
	 * @param reportId
	 *            The report id that must be one of the values returned by the
	 *            <code>getReportIds</code> method.
	 * @param reportType
	 *            The report type which cannot be null.
	 * @param startTime
	 *            The start time which cannot be null.
	 * @param endTime
	 *            The end time which cannot be null and must be equal to or
	 *            later than the start time.
	 * @param filter
	 *            A named filter or null if none.
	 * @param filterMode
	 *            The filter mode.
	 * @param rowIndex
	 *            The zero-based row index in the report dataset to start the
	 *            export at.
	 * @param rowCount
	 *            The number of dataset rows to return. Google Analytics won't
	 *            return more than 500 rows, setting this higher than 500 will
	 *            still give you 500 rows.
	 * @return The <code>InputStream</code> object.
	 * @throws JAnalyticsException
	 */
	public String getReportString(String reportId, ReportTypes reportType,
			Calendar startTime, Calendar endTime, String filter,
			FilterModes filterMode, int rowIndex, int rowCount)
			throws JAnalyticsException {
		startTime = Utilities.trimCalendar(startTime);
		endTime = Utilities.trimCalendar(endTime);

		ArrayList pairs = new ArrayList();

		pairs.add(new NameValuePair("fmt", "1"));
		pairs.add(new NameValuePair("id", reportId));
		pairs.add(new NameValuePair("pdr", Utilities
				.calendarToString(startTime)
				+ "-" + Utilities.calendarToString(endTime)));
		pairs.add(new NameValuePair("cmp", "average"));
		pairs
				.add(new NameValuePair("rpt", reportType
						.getReportAttributeValue()));
		pairs.add(new NameValuePair(ROW_INDEX_ATTRIBUTE_NAME, String
				.valueOf(rowIndex)));
		pairs.add(new NameValuePair(ROW_COUNT_ATTRIBUTE_NAME, String
				.valueOf(rowCount)));

		if (filter != null && filterMode != FilterModes.NONE) {
			pairs.add(new NameValuePair(FILTER_ATTRIBUTE_NAME, filter));
			pairs.add(new NameValuePair(FILTER_MODE_ATTRIBUTE_NAME, filterMode
					.getValueAsString()));
		}

		NameValuePair[] drillDownPairs = reportType.getDrillDownParams();
		NameValuePair[] nameValuePairs = new NameValuePair[pairs.size()
				+ drillDownPairs.length];
		System.arraycopy(pairs.toArray(new NameValuePair[0]), 0,
				nameValuePairs, 0, pairs.size());

		if (drillDownPairs.length > 0) {
			System.arraycopy(drillDownPairs, 0, nameValuePairs, pairs.size(),
					drillDownPairs.length);
		}

		GetMethod getMethod = new GetMethod(
				"https://www.google.com/analytics/reporting/export");
		getMethod.setQueryString(nameValuePairs);

		String reportString = null;
		try {
			client.executeMethod(getMethod);
			int statuscode = getMethod.getStatusCode();
			if (statuscode == HttpStatus.SC_OK) {
				getMethod.setRequestHeader("Content-Type", "UTF-8");
				getMethod.setRequestHeader("Content-Length", "5");

				reportString = getMethod.getResponseBodyAsString();
				
				latestFetchedSource = reportString;
			} else {
				throw new JAnalyticsException(
						"Exception while getting report: http status code is "
								+ statuscode);
			}

		} catch (IOException e) {
			throw new JAnalyticsException("Exception while getting report: "
					+ e.getMessage(), e);
		} finally {
			if (getMethod != null) {
				getMethod.releaseConnection();
			}
		}
		return reportString;
	}

	/**
	 * Gets the given report as an <code>InputStream</code> object.
	 * 
	 * @param reportId
	 *            The report id that must be one of the values returned by the
	 *            <code>getReportIds</code> method.
	 * @param reportType
	 *            The report type which cannot be null.
	 * @param startTime
	 *            The start time which cannot be null.
	 * @param endTime
	 *            The end time which cannot be null and must be equal to or
	 *            later than the start time.
	 * @param filter
	 *            A named filter or null if none.
	 * @param filterMode
	 *            The filter mode.
	 * @param rowIndex
	 *            The zero-based row index in the report dataset to start the
	 *            export at.
	 * @param rowCount
	 *            The number of dataset rows to return. Google Analytics won't
	 *            return more than 500 rows, setting this higher than 500 will
	 *            still give you 500 rows.
	 * @return The <code>InputStream</code> object.
	 * @throws JAnalyticsException
	 */
	public InputStream getReportInputStream(String reportId,
			ReportTypes reportType, Calendar startTime, Calendar endTime,
			String filter, FilterModes filterMode, int rowIndex, int rowCount)
			throws JAnalyticsException {
		InputStream in = null;
		try {
			in = new ByteArrayInputStream(getReportString(reportId, reportType,
					startTime, endTime, filter, filterMode, rowIndex, rowCount)
					.getBytes("UTF-8"));

		} catch (IOException e) {
			throw new JAnalyticsException("Exception while getting report: "
					+ e.getMessage(), e);
		} finally {
		}
		return in;
	}

	/**
	 * Main.
	 * 
	 * @param args
	 *            The array of email, password arguments.
	 * @throws JAnalyticsException
	 */
	public static void main(String[] args) throws JAnalyticsException {

		if (args.length != 2) {
			throw new JAnalyticsException(
					"Parameters missing: accountname@gmail.com password");
		}

		JAnalytics jAnalytics = new JAnalytics();
		jAnalytics.login(args[0], args[1]);
		String[] reportIds = jAnalytics.getReportIds();
		for (int i = 0, count = reportIds.length; i < count; i++) {
			System.out.println("Report id found: " + reportIds[i]);
		}
		Calendar start = Calendar.getInstance();
		Calendar end = Calendar.getInstance();
		start.roll(Calendar.DAY_OF_YEAR, -7);
		System.out.println(jAnalytics.getReport(jAnalytics.getReportIds()[0],
				ReportTypes.GEO_MAP_DETAIL_CITY_REPORT, start, end, null,
				FilterModes.NONE, 0, DEFAULT_ROW_COUNT));
	}

	/**
	 * Gets the latest fetched source returned from the internal HttpClient.
	 * 
	 * @return the latest fetched source
	 */
	public String getLatestFetchedSource() {
		return latestFetchedSource;
	}

}
